# -*- coding: utf-8 -*-

import numpy as np


class KMeansClassifier():

    def __init__(self, k=3, initCent='random', max_iter=5000):
        #构造函数，初始化相关属性
        self._k = k
        self._initCent = initCent  # 初始中心
        self._max_iter = max_iter  # 最大迭代
        # 一个m*2的二维矩阵，矩阵第一列存储样本点所属的族的索引值，
        # 第二列存储该点与所属族的质心的平方误差
        self._clusterAssment = None  # 样本点聚类结结构矩阵
        self._labels = None
        self._sse = None  # SSE（Sum of squared errors）平方误差和

    def _calEDist(self, arrA, arrB):
        #欧式距离距离计算
        #输入：两个一维数组
        arrA_temp = arrA.copy()
        arrB_temp = arrB.copy()
        arrA_temp[0] = arrA_temp[0] * 0.0875#总价映射比例
        arrA_temp[1] = arrA_temp[1] * 0.0007#单价映射比例0.005
        arrB_temp[0] = arrB_temp[0] * 0.0875
        arrB_temp[1] = arrB_temp[1] * 0.0007
        return np.math.sqrt(sum(np.power(arrA_temp - arrB_temp, 2)))#[arrA_temp[0]-arrB_temp[0]]^2+[arrA_temp[1]-arrB_temp[1]]^2

    def _randCent(self, data_X, k):
        #随机选取k个质心
        #输出：centroids #返回一个m*n的质心矩阵
        n = data_X.shape[1] - 3  # 获取特征值的维数(要删除一个用于标记的id列和经纬度值)
        centroids = np.empty((k, n))  # 使用numpy生成一个k*n的矩阵，用于存储质心
        for j in range(n):
            minJ = min(data_X[:, j + 1])
            rangeJ = max(data_X[:, j + 1] - minJ)
            # 使用flatten拉平嵌套列表(nested list)
            centroids[:, j] = (minJ + rangeJ * np.random.rand(k, 1)).flatten()#[[1,2],[3,4],[5,6]]->[1,2,3,4,5,6]
        return centroids

    def fit(self, data_X):
        #输入：一个m*n维的矩阵
        if not isinstance(data_X, np.ndarray) or \
                isinstance(data_X, np.matrixlib.defmatrix.matrix):
            try:
                data_X = np.asarray(data_X)
            except:
                raise TypeError("numpy.ndarray resuired for data_X")

        m = data_X.shape[0]  # 获取样本的个数
        # 一个m*2的二维矩阵，矩阵第一列存储样本点所属的族的编号，
        # 第二列存储该点与所属族的质心的平方误差
        self._clusterAssment = np.zeros((m, 2))

        # 创建k个点，作为起始质心
        if self._initCent == 'random':
            self._centroids = self._randCent(data_X, self._k)

        clusterChanged = True
        # 循环最大迭代次数
        for _ in range(self._max_iter):  # 使用"_"主要是因为后面没有用到这个值
            clusterChanged = False
            for i in range(m):  # 将每个样本点分配到离它最近的质心所属的族
                minDist = np.inf  # 首先将minDist置为一个无穷大的数
                minIndex = -1  # 将最近质心的下标置为-1
                for j in range(self._k):  # 次迭代用于寻找元素最近的质心
                    arrA = self._centroids[j, :]
                    arrB = data_X[i, 1:4]
                    distJI = self._calEDist(arrA, arrB)  # 计算距离
                    if distJI < minDist:
                        minDist = distJI
                        minIndex = j
                if self._clusterAssment[i, 0] != minIndex or self._clusterAssment[i, 1] > minDist ** 2:
                    clusterChanged = True
                    self._clusterAssment[i, :] = minIndex, minDist ** 2
            if not clusterChanged:  # 若所有样本点所属的族都不改变,则已收敛,结束迭代
                break
            for i in range(self._k):  # 更新质心，将每个族中的点的均值作为质心
                index_all = self._clusterAssment[:, 0]  # 取出样本所属簇的编号
                value = np.nonzero(index_all == i)  # 取出所有属于第i个簇的索引值
                ptsInClust = data_X[value[0]]  # 取出属于第i个簇的所有样本点
                if len(ptsInClust) !=0 :#非空判断
                    self._centroids[i, :] = np.mean(ptsInClust[:, 1:4], axis=0)  # 计算均值,赋予新的质心

        self._labels = self._clusterAssment[:, 0]
        self._sse = sum(self._clusterAssment[:, 1])

    def predict(self, X):  # 根据聚类结果，预测新输入数据所属的族
        # 类型检查
        if not isinstance(X, np.ndarray):
            try:
                X = np.asarray(X)
            except:
                raise TypeError("numpy.ndarray required for X")

        m = X.shape[0]  # m代表样本数量
        preds = np.empty((m,))
        for i in range(m):  # 将每个样本点分配到离它最近的质心所属的族
            minDist = np.inf
            for j in range(self._k):
                distJI = self._calEDist(self._centroids[j, :], X[i, :])
                if distJI < minDist:
                    minDist = distJI
                    preds[i] = j
        return preds
